/*******************************************************************************
 * Descharts library
 * Copyright (c) 2014 Bradipao <bradipao@gmail.com>
 * https://plus.google.com/+SidBradipao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package it.bradipao.lib.descharts;

import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrHelper;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.render.PathEffect;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.render.Texture;
import ohos.agp.text.Font;
import ohos.agp.utils.Color;
import ohos.agp.utils.TextAlignment;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;

/**
 * CartesianView
 *
 * @since 2021-08-09
 */
public class CartesianView extends Component implements Component.DrawTask {
    private static final float ONW = 7.0f;
    private static final int TEMP1 = 0;
    private static final int TEMP2 = 8;
    private static final String TEMP3 = "#FF000000";

    int mWidth = TEMP1;
    int mHeight = TEMP1;
    int mPaddtop = TEMP2;
    int mPaddright = TEMP2;
    int mPaddbottom = TEMP2;
    int mPaddleft = TEMP2;

    // visibility and appearance
    boolean isXscaleAuto = true;
    boolean isYscaleAuto = true;
    boolean isBorderVis = true;
    boolean isGridVis = true;
    boolean isAxisVis = true;
    boolean isXtextVis = true;
    boolean isYtextVis = true;
    boolean isXtextBottom = true;
    boolean isYtextLeft = true;
    boolean isGridAa = true;

    // color params
    int mBackgroundColor = Color.getIntColor("#FFFFFFFF");
    int mBorderColor = Color.getIntColor(TEMP3);
    int mGridColor = Color.getIntColor(TEMP3);
    int mAxisColor = Color.getIntColor(TEMP3);
    int mTextColor = Color.getIntColor(TEMP3);

    // size params
    float mBorderWidth = dipToPixel(1.0f);
    float mGridWidth = dipToPixel(1.0f);
    float mAxisWidth = dipToPixel(1.0f);
    float mTextSize = dipToPixel(ONW);

    boolean isRedraw = false;
    float mX;
    float mY;
    float mDx;
    float mDy;
    float mEx;
    float mEy;
    float mXmin;
    float mXmax;
    float mYmin;
    float mYmax;
    float mXminGrid;
    float mXmaxGrid;
    float mYminGrid;
    float mYmaxGrid;
    float mXdivGrid;
    float mYdivGrid;
    int mXgridNum;
    int mYgridNum;
    float mAx;
    float mBx;
    float mAy;
    float mBy;

    Canvas mCnv = null;
    PixelMap mBmp = null;
    Paint mPntBorder = new Paint();
    Paint mPntGrid = new Paint();
    Paint mPntAxis = new Paint();
    Paint mPntText = new Paint();
    Paint mBmpPant = new Paint();
    Path mPath = new Path();

    int mI;
    int mJ;
    float ff;

    /**
     * Constructor.
     *
     * @param context context
     */
    public CartesianView(Context context) {
        super(context);
        init();
    }

    /**
     * Constructor.
     *
     * @param context context
     * @param attrs attrs
     */
    public CartesianView(Context context, AttrSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    public void onDraw(Component component, Canvas cnv) {
        // create cache bitmap if necessary
        if ((mBmp == null) || isRedraw) {
            getViewSizes();
            getxyminmax();
            if (isXscaleAuto) {
                calcXgridRange();
            }
            if (isYscaleAuto) {
                calcYgridRange();
            }
            calcxycoefs();
            PixelMap.InitializationOptions options = new PixelMap.InitializationOptions();
            options.pixelFormat = PixelFormat.ARGB_8888;
            options.size = new Size(mWidth, mHeight);
            mBmp = PixelMap.create(options);
            Texture texture = new Texture(mBmp);
            mCnv = new Canvas(texture);
            if (isGridVis) {
                drawGrid();
            }
            if (isXtextVis) {
                drawXlabel();
            }
            if (isYtextVis) {
                drawYlabel();
            }
            if (isBorderVis) {
                drawBorder();
            }
            if (isAxisVis) {
                drawAxis();
            }
            isRedraw = false;
        }

        cnv.drawPixelMapHolder(new PixelMapHolder(mBmp), 0, 0, mBmpPant);
    }

    /**
     * Sets view padding.
     *
     * @param pad pad
     */
    public void setPadding(int pad) {
        mPaddtop = pad;
        mPaddright = pad;
        mPaddbottom = pad;
        mPaddleft = pad;
    }

    /**
     * Sets view padding in DIP.
     *
     * @param pad pad
     */
    public void setPaddingDip(int pad) {
        mPaddtop = (int) dipToPixel(pad);
        mPaddright = (int) dipToPixel(pad);
        mPaddbottom = (int) dipToPixel(pad);
        mPaddleft = (int) dipToPixel(pad);
    }

    /**
     * Sets view padding.
     *
     * @param paddtop paddtop
     * @param padright padright
     * @param paddbot paddbot
     * @param padleft padleft
     */
    public void setPadding(int paddtop, int padright, int paddbot, int padleft) {
        mPaddtop = paddtop;
        mPaddright = padright;
        mPaddbottom = paddbot;
        mPaddleft = padleft;
    }

    /**
     * Sets view padding in DIP.
     *
     * @param paddtop paddtop
     * @param padright padright
     * @param paddbot paddbot
     * @param padleft padleft
     */
    public void setPaddingDip(int paddtop, int padright, int paddbot, int padleft) {
        mPaddtop = (int) dipToPixel(paddtop);
        mPaddright = (int) dipToPixel(padright);
        mPaddbottom = (int) dipToPixel(paddbot);
        mPaddleft = (int) dipToPixel(padleft);
    }

    /**
     * Sets view background color.
     *
     * @param color color
     */
    public void setBackgroundColor(int color) {
        mBackgroundColor = color;
        ShapeElement backShape = new ShapeElement();
        backShape.setRgbColor(RgbColor.fromArgbInt(color));
        setBackground(backShape);
    }

    /**
     * Sets X grid autoscale and (if false) grid range and number of steps.
     *
     * @param autoXscale autoXscale
     * @param xmin xmin
     * @param xmax xmax
     * @param num num
     */
    public void setXgrid(boolean autoXscale, float xmin, float xmax, int num) {
        isXscaleAuto = autoXscale;
        if (!autoXscale) {
            mXminGrid = xmin;
            mXmaxGrid = xmax;
            mXgridNum = num;
            mXdivGrid = (xmax - xmin) / num;
        }
        invalidate();
    }

    /**
     * Sets Y grid autoscale and (if false) grid range and number of steps.
     *
     * @param autoYscale autoYscale
     * @param ymin ymin
     * @param ymax ymax
     * @param num num
     */
    public void setYgrid(boolean autoYscale, float ymin, float ymax, int num) {
        isYscaleAuto = autoYscale;
        if (!autoYscale) {
            mYminGrid = ymin;
            mYmaxGrid = ymax;
            mYgridNum = num;
            mYdivGrid = (ymax - ymin) / num;
        }
        invalidate();
    }

    /**
     * Sets border, grid and axis visibility.
     *
     * @param bBorderShow bBorderShow
     * @param bGridShow bGridShow
     * @param bAxisShow bAxisShow
     */
    public void setGridVis(boolean bBorderShow, boolean bGridShow, boolean bAxisShow) {
        isBorderVis = bBorderShow;
        isGridVis = bGridShow;
        isAxisVis = bAxisShow;
        isRedraw = true;
        invalidate();
    }

    /**
     * Sets border, grid and axis color.
     *
     * @param borderColor borderColor
     * @param gridColor gridColor
     * @param axisColor axisColor
     */
    public void setGridColor(int borderColor, int gridColor, int axisColor) {
        mBorderColor = borderColor;
        mGridColor = gridColor;
        mAxisColor = axisColor;
        init();
        isRedraw = true;
        invalidate();
    }

    /**
     * Sets border, grid and axis width.
     *
     * @param borderWidth borderWidth
     * @param gridWidth gridWidth
     * @param axisWidth axisWidth
     */
    public void setGridWidth(float borderWidth, float gridWidth, float axisWidth) {
        mBorderWidth = borderWidth;
        mGridWidth = gridWidth;
        mAxisWidth = axisWidth;
        init();
        isRedraw = true;
        invalidate();
    }

    /**
     * Sets border, grid and axis width in DIP.
     *
     * @param borderWidth borderWidth
     * @param gridWidth gridWidth
     * @param axisWidth axisWidth
     */
    public void setGridWidthDip(float borderWidth, float gridWidth, float axisWidth) {
        mBorderWidth = dipToPixel(borderWidth);
        mGridWidth = dipToPixel(gridWidth);
        mAxisWidth = dipToPixel(axisWidth);
        init();
        isRedraw = true;
        invalidate();
    }

    /**
     * Enables AntiAlias rendering for border, grid and axis.
     *
     * @param antialias antialias
     */
    public void setGridAA(boolean antialias) {
        isGridAa = antialias;
        init();
        isRedraw = true;
        invalidate();
    }

    /**
     * Sets X text and Y text visibility and position.
     *
     * @param xtext xtext
     * @param ytext ytext
     * @param xbottom xbottom
     * @param yleft yleft
     */
    public void setTextVis(boolean xtext, boolean ytext, boolean xbottom, boolean yleft) {
        isXtextVis = xtext;
        isYtextVis = ytext;
        isXtextBottom = xbottom;
        isYtextLeft = yleft;
        isRedraw = true;
        invalidate();
    }

    /**
     * Sets text color and size.
     *
     * @param color color
     * @param size size
     */
    public void setTextStyle(int color, float size) {
        mTextColor = color;
        mTextSize = dipToPixel(size);
        init();
        isRedraw = true;
        invalidate();
    }

    /**
     * Inits Paint objects
     */
    protected void init() {
        mPntBorder.setStyle(Paint.Style.STROKE_STYLE);
        mPntBorder.setColor(new Color(mBorderColor));
        mPntBorder.setStrokeWidth(mBorderWidth);
        mPntBorder.setAntiAlias(isGridAa);
        mPntGrid.setStyle(Paint.Style.STROKE_STYLE);
        mPntGrid.setColor(new Color(mGridColor));
        mPntGrid.setStrokeWidth(mGridWidth);
        mPntGrid.setPathEffect(new PathEffect(new float[]{2, 2}, 0));
        mPntGrid.setAntiAlias(isGridAa);
        mPntAxis.setStyle(Paint.Style.STROKE_STYLE);
        mPntAxis.setColor(new Color(mAxisColor));
        mPntAxis.setStrokeWidth(mAxisWidth);
        mPntAxis.setAntiAlias(isGridAa);
        mPntText.setColor(new Color(mTextColor));
        mPntText.setFont(Font.SANS_SERIF);
        mPntText.setTextSize((int) mTextSize);
        mPntText.setStyle(Paint.Style.FILL_STYLE);
        mPntText.setAntiAlias(true);

        setBackgroundColor(mBackgroundColor);
        addDrawTask(this);
    }

    /**
     * Gets view sizes
     */
    protected void getViewSizes() {
        // params from view
        mWidth = getWidth();
        mHeight = getHeight();
        mX = mPaddleft;
        mY = mPaddtop;
        mEx = mWidth - mPaddright;
        mEy = mHeight - mPaddbottom;
        if (isYtextVis && isYtextLeft) {
            mX += 3 * mTextSize;
        }
        if (isYtextVis && !isYtextLeft) {
            mEx -= 3 * mTextSize;
        }
        if (isXtextVis && isXtextBottom) {
            mEy -= mTextSize + 2;
        }
        if (isXtextVis && !isXtextBottom) {
            mY += mTextSize + 2;
        }
        mDx = mEx - mX;
        mDy = mEy - mY;
    }

    /**
     * Sets dummy values for X,Y ranges
     */
    protected void getxyminmax() {
        mXmin = -9;
        mXmax = 9;
        mYmin = -90;
        mYmax = 90;
    }

    /**
     * Automatic calculation of X grid range
     */
    protected void calcXgridRange() {
        mXdivGrid = (float) Math.pow(10, Math.floor(Math.log10(Math.abs(mXmax - mXmin))));
        mXminGrid = (float) (mXdivGrid * Math.floor(mXmin / mXdivGrid));
        mXmaxGrid = (float) (mXdivGrid * Math.ceil(mXmax / mXdivGrid));
        mXgridNum = (int) ((mXmaxGrid - mXminGrid) / mXdivGrid);
        if ((mDx / mDy) < 1.2) {
            if (mXgridNum <= 2) {
                mXgridNum *= 5;
            } else if (mXgridNum == 3) {
                mXgridNum *= 3;
            } else if (mXgridNum <= 5) {
                mXgridNum *= 2;
            }
        } else {
            if (mXgridNum <= 2) {
                mXgridNum *= 6;
            } else if (mXgridNum == 3) {
                mXgridNum *= 4;
            } else if (mXgridNum == 4) {
                mXgridNum *= 3;
            } else if (mXgridNum <= 6) {
                mXgridNum *= 2;
            }
        }
    }

    /**
     * Automatic calculation of Y grid range
     */
    protected void calcYgridRange() {
        mYdivGrid = (float) Math.pow(10, Math.floor(Math.log10(Math.abs(mYmax - mYmin))));
        mYminGrid = (float) (mYdivGrid * Math.floor(mYmin / mYdivGrid));
        mYmaxGrid = (float) (mYdivGrid * Math.ceil(mYmax / mYdivGrid));
        mYgridNum = (int) ((mYmaxGrid - mYminGrid) / mYdivGrid);
        if ((mDy / mDx) < 1.2) {
            if (mYgridNum <= 2) {
                mYgridNum *= 5;
            } else if (mYgridNum <= 3) {
                mYgridNum *= 3;
            } else if (mYgridNum <= 5) {
                mYgridNum *= 2;
            }
        } else {
            if (mYgridNum <= 2) {
                mYgridNum *= 6;
            } else if (mYgridNum == 3) {
                mYgridNum *= 4;
            } else if (mYgridNum == 4) {
                mYgridNum *= 3;
            } else if (mYgridNum <= 6) {
                mYgridNum *= 2;
            }
        }
    }

    /**
     * Calculates drawing coefficients
     */
    protected void calcxycoefs() {
        mAx = (float) mDx / Math.abs(mXmaxGrid - mXminGrid);
        mBx = (float) mXminGrid;
        mAy = (float) mDy / Math.abs(mYmaxGrid - mYminGrid);
        mBy = (float) mYminGrid;
    }

    /**
     * Draw inner grid
     */
    protected void drawGrid() {
        mPath.reset();
        for (mI = 1; mI < mXgridNum; mI++) {
            mPath.moveTo(mX + mI * (mDx / mXgridNum), mY);
            mPath.lineTo(mX + mI * (mDx / mXgridNum), mEy);
        }
        for (mI = 1; mI < mYgridNum; mI++) {
            mPath.moveTo(mX, mY + mI * (mDy / mYgridNum));
            mPath.lineTo(mEx, mY + mI * (mDy / mYgridNum));
        }
        mCnv.drawPath(mPath, mPntGrid);
    }

    /**
     * Draw X label on top or bottom
     */
    protected void drawXlabel() {
        mPntText.setTextAlign(TextAlignment.CENTER);
        mPath.reset();
        if (isXtextBottom) {
            for (mI = 1; mI < mXgridNum; mI++) {
                mPath.moveTo(mX + mI * (mDx / mXgridNum), mEy - 3);
                mPath.lineTo(mX + mI * (mDx / mXgridNum), mEy + 3);
                ff = mXminGrid + mI * (mXmaxGrid - mXminGrid) / mXgridNum;
                mCnv.drawText(mPntText, String.format("%.1f", ff), mX + mI * (mDx / mXgridNum), mEy + mTextSize + 2);
            }
        } else {
            for (mI = 1; mI < mXgridNum; mI++) {
                mPath.moveTo(mX + mI * (mDx / mXgridNum), mY - 3);
                mPath.lineTo(mX + mI * (mDx / mXgridNum), mY + 3);
                ff = mXminGrid + mI * (mXmaxGrid - mXminGrid) / mXgridNum;
                mCnv.drawText(mPntText, String.format("%.1f", ff), mX + mI * (mDx / mXgridNum), mY - 8);
            }
        }
        mCnv.drawPath(mPath, mPntAxis);
    }

    /**
     * Draw Y label on left or right
     */
    protected void drawYlabel() {
        if (isYtextLeft) {
            mPntText.setTextAlign(TextAlignment.RIGHT);
        } else {
            mPntText.setTextAlign(TextAlignment.LEFT);
        }
        mPath.reset();
        if (isYtextLeft) {
            for (mI = 1; mI < mYgridNum; mI++) {
                mPath.moveTo(mX - 3, mEy - mI * (mDy / mYgridNum));
                mPath.lineTo(mX + 3, mEy - mI * (mDy / mYgridNum));
                ff = mYminGrid + mI * (mYmaxGrid - mYminGrid) / mYgridNum;
                mCnv.drawText(mPntText, String.format("%.1f", ff), mX - 6, mEy - mI * (mDy / mYgridNum) + mTextSize / 2);
            }
        } else {
            for (mI = 1; mI < mYgridNum; mI++) {
                mPath.moveTo(mEx - 3, mEy - mI * (mDy / mYgridNum));
                mPath.lineTo(mEx + 3, mEy - mI * (mDy / mYgridNum));
                ff = mYminGrid + mI * (mYmaxGrid - mYminGrid) / mYgridNum;
                mCnv.drawText(mPntText, String.format("%.1f", ff), mEx + 6, mEy - mI * (mDy / mYgridNum) + mTextSize / 2);
            }
        }
        mCnv.drawPath(mPath, mPntAxis);
    }

    /**
     * Draw outer border
     */
    protected void drawBorder() {
        mPath.reset();
        mPath.moveTo(mX, mY);
        mPath.lineTo(mEx, mY);
        mPath.lineTo(mEx, mEy);
        mPath.lineTo(mX, mEy);
        mPath.lineTo(mX, mY);
        mCnv.drawPath(mPath, mPntBorder);
    }

    /**
     * Draw X and Y axis
     */
    protected void drawAxis() {
        mPath.reset();
        mPath.moveTo(mX - mBx * mAx, mY);
        mPath.lineTo(mX - mBx * mAx, mEy);
        mPath.moveTo(mX, mEy + mBy * mAy);
        mPath.lineTo(mEx, mEy + mBy * mAy);
        mCnv.drawPath(mPath, mPntAxis);
    }

    /**
     * Converts value expressed in dp (device independent pixel) in value
     * expressed in actual display pixel (depends on display metrics).
     *
     * @param dips dips
     * @return float
     */
    protected float dipToPixel(float dips) {
        return AttrHelper.vp2px(dips, getContext()) - 0.6f;
    }
}
